// 
// Entify project
//  
// Author:
//       Lauri Taimila <lauri@taimila.com>
// 
// Copyright (c) 2010 Lauri Taimila
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Text;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;

namespace Taimila.Entify
{
	/// <summary>
	/// Base class for all entity types. Implements lazy loading for sub entities (relations)
	/// and some public methods for entity handling.
	/// </summary>
	public abstract class Entity
	{
		/// <summary>
		/// Metadata of the entity. Metadata object's fields are empty
		/// until entity has been added to entity context.
		/// </summary>
		private readonly EntityMetadata metadata;
		
		/// <summary>
		/// Create new entity instance. Creates metadata container for entity.
		/// </summary>
		public Entity()
		{
			this.metadata = new EntityMetadata();
		}
		
		/// <summary>
		/// Gets the entity identifier of this entity. This is called from generated
		/// entity code via reflection.
		/// </summary>
		/// <returns>
		/// Entity identifier <see cref="System.Nullable<System.UInt64>"/>
		/// </returns>
		private long? GetEntityId()
		{
			return metadata.EntityId;
		}
		
		/// <summary>
		/// Loads sub entity from the entity source context with the given entity identifier.
		/// </summary>
		/// <param name="entityIdentifier">
		/// Entity identifier of the entity to be loaded.
		/// </param>
		/// <returns>
		/// Loaded entity object. <see cref="Entity"/>
		/// </returns>
		protected T LazyLoadEntity<T>(long? entityIdentifier) where T: Entity
		{
			if(this is T && entityIdentifier == this.metadata.EntityId)
			{
				return this as T;	
			}

			if(entityIdentifier == null)
			{
				return null;
			}
			
			EntityRequest<T> request = new EntityRequest<T>();
			request.DataFilter = new ValueFilter("[Entity Id]", entityIdentifier);
			
			try
			{
				T[] entities = this.metadata.sourceContext.GetEntities(request);
				return entities.Single();
			}
			catch
			{
				throw new EntityNotFoundException("Unable to load entity type '" + typeof(T).FullName + "' with id '" + entityIdentifier + "'.");
			}
		}
		
		/// <summary>
		/// Loads sub entities from the entity source context with the given entity identifiers.
		/// </summary>
		/// <param name="entityIdentifiers">
		/// Entity identifiers of the entities to be loaded.
		/// </param>
		/// <returns>
		/// List of loaded entities. <see cref="Entity"/>
		/// </returns>
		protected List<T> LazyLoadEntities<T>(List<long?> entityIdentifiers) where T: Entity
		{
			if(entityIdentifiers == null || entityIdentifiers.Count == 0)
			{
				return new List<T>();
			}

			string[] identifiers = (from id in entityIdentifiers select id.ToString()).ToArray();

			EntityRequest<T> request = new EntityRequest<T>();
			request.DataFilter = new ValueSetFilter("[Entity Id]", identifiers);

			try
			{
				T[] entities = this.metadata.sourceContext.GetEntities(request);
				return entities.ToList();
			}
			catch
			{
				throw new EntityNotFoundException("Unable to lazy load entities of type '" + typeof(T).FullName + "'.");
			}
		}
		
		/// <summary>
		/// Determines are the current and given object the same entity.
		/// If objects are same type and they have same entity id then
		/// entities are same, otherwise not. This method compares only
		/// identities of the entities, not their properties! If entities
		/// are not in entity context they cannot be same. Use Equals()
		/// for equality check!
		/// </summary>
		/// <param name="entity">
		/// Entity to compare with <see cref="System.Object"/>
		/// </param>
		/// <returns>
		/// True if entities are same, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		public bool IsSame(object entity)
		{
			try
			{
				long? otherId = (entity as Entity).GetEntityId();
				
				if(entity.GetType() == this.GetType() && this.metadata.EntityId == otherId && this.metadata.EntityId != null)
				{
					return true;	
				}
				else
				{
					return false;	
				}
			}
			catch
			{
				return false;
			}
		}
		
		/// <summary>
		/// Returns true if this entity is in the entity context, otherwise false.
		/// </summary>
		/// <returns>
		/// True if this entity is in the entity context, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		public bool IsInEntityContext()
		{
			if(this.GetSourceContext() == null)
			{
				return false;	
			}
			else
			{
				return true;
			}
		}
		
		/// <summary>
		/// Get string presentation of the entity.
		/// </summary>
		/// <returns>
		/// Entity as string. <see cref="System.String"/>
		/// </returns>
		public override string ToString()
		{
			StringBuilder s = new StringBuilder();
			s.AppendLine("Instance of an entity object");
			s.AppendLine("Type: " + this.GetType().FullName);
			s.AppendLine("Version: " + AttributeReader.GetEntityVersion(this.GetType()));
			s.AppendLine("Id: " + this.GetEntityId());
			s.AppendLine("Last update: " + this.GetLastModifiedDate());

			foreach(var p in this.GetType().GetProperties())
			{
				if(AttributeReader.HasEntityProperty(p))
				{
					s.AppendLine(p.Name + ": " + p.GetValue(this, null));	
				}
			}
			
			return s.ToString();
		}
		
		/// <summary>
		/// Implements Equals() for all entity types. Entities are considered to be 
		/// equal if they have same values in entity properties.
		/// </summary>
		/// <param name="obj">
		/// Object to compare with. <see cref="System.Object"/>
		/// </param>
		/// <returns>
		/// Is given entity equals to this entity. <see cref="System.Boolean"/>
		/// </returns>
		public override bool Equals(object obj)
		{
			try
			{
				Type t = this.GetType();
				Type t2 = obj.GetType();
				
				if(t != t2)
				{
					return false;	
				}
				
				foreach(var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
				{
					if(AttributeReader.HasEntityProperty(p))
					{	
						if(!this.AreValuesSame(p.GetValue(this, null), p.GetValue(obj, null)))
						{
							return false;
						}
					}
				}
				
				return true;
			}
			catch
			{
				return false;	
			}
		}
		
		/// <summary>
		/// This is a helper method for Equals() implementation. Checks
		/// are the specified values same.
		/// </summary>
		/// <param name="a">
		/// First object to compare. <see cref="System.Object"/>
		/// </param>
		/// <param name="b">
		/// Second object to compare. <see cref="System.Object"/>
		/// </param>
		/// <returns>
		/// True, if specified values are same, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		private bool AreValuesSame(object a, object b)
		{
			if(a == null || b == null)
			{
				if(a == null && b == null)
				{
					return true;	
				}
				else
				{
					return false;
				}
			}
			else
			{
				Type t = a.GetType();
				Type t2 = b.GetType();
				
				if(t == t2)
				{
					return a.Equals(b);	
				}
				else
				{
					return false;	
				}
			}
		}
		
		/// <summary>
		/// Get hash representiation of this object.
		/// </summary>
		/// <returns>
		/// Hashcode of the entity. <see cref="System.Int32"/>
		/// </returns>
		public override int GetHashCode()
		{
			int hashcode = 0;
			
			foreach(var f in this.GetType().GetFields())
			{
				var fieldValue = f.GetValue(this);
				
				if(fieldValue != null)
				{
					hashcode ^= fieldValue.GetHashCode();
				}
			}
			
			foreach(var p in this.GetType().GetProperties())
			{
				// Skip relation properties, because it would lead to unwanted lazy loads
				if(!AttributeReader.IsRelationship(p))
				{
					var propertyValue = p.GetValue(this, null);
					
					if(propertyValue != null)
					{
						hashcode ^= propertyValue.GetHashCode();
					}
				}
			}
			
			return hashcode;
		}
	}
}
